﻿using System;
using System.Diagnostics;
using System.IO;

namespace SfsTools.Core
{
    /// <summary>
    /// Provides access to individual data area blocks.
    /// </summary>
    internal class DataArea
    {
        #region Properties

        /// <summary>
        /// Contains the volume.
        /// </summary>
        private readonly IVolume FVolume;
        /// <summary>
        /// Contains the map about the free/used data area blocks.
        /// </summary>
        private readonly DataAreaBlockRegionMap FDataMap;
        /// <summary>
        /// Contains the size of a block.
        /// </summary>
        private Int32 FBlockSize;
        // <summary>
        // Contains the starting position of the data area.
        // </summary>
        //private readonly Int64 FPosition;
        /// <summary>
        /// Contains the length of the data area in blocks.
        /// </summary>
        private readonly Int64 FLength;
        /// <summary>
        /// Gets the length of the data area in blocks.
        /// </summary>
        public Int64 Length
        {
            [DebuggerStepThrough]
            get
            {
                return FLength;
            }
        }
        /// <summary>
        /// Contains the length of the volume in blocks.
        /// </summary>
        private readonly Int64 FVolumeLength;
        /// <summary>
        /// Gets the length of the volume in blocks.
        /// </summary>
        public Int64 VolumeLength
        {
            [DebuggerStepThrough]
            get
            {
                return FVolumeLength;
            }
        }

        #endregion

        #region Methods

        #region Misc

        /// <summary>
        /// Sets the block size to <paramref name="value"/>.
        /// </summary>
        /// <param name="value">The size of a block, in bytes.</param>
        [DebuggerStepThrough]
        public void SetBlockSize(int value)
        {
            FBlockSize = value;
        }

        /// <summary>
        /// For testing.
        /// </summary>
        /// <returns></returns>
        [DebuggerStepThrough]
        internal BlockRegionPair[] GetMap()
        {
            return FDataMap.GetMap();
        }

        #endregion

        /// <summary>
        /// Allocates a region of blocks in the data area.
        /// </summary>
        /// <param name="length">The length of the file, in bytes, which must be larger than zero.</param>
        /// <returns>A <see cref="BlockRegion"/> that was allocated to the file.</returns>
        /// <exception cref="DataAreaFullException">The data area is full.</exception>
        [DebuggerStepThrough]
        public BlockRegion AllocateFile(Int64 length)
        {
            long blockCount = (length + FBlockSize - 1) / FBlockSize;
            BlockRegion br = FDataMap.FindAndAllocateBlocks(blockCount);
            return br;
        }

        /// <summary>
        /// Attempts to allocate the <paramref name="region"/> specified.
        /// </summary>
        /// <param name="region">The region to be allocated.</param>
        /// <exception cref="DataAreaFullException">The requested region cannot be allocated.</exception>
        /// <remarks>Used for appending new data area blocks to an existing file.</remarks>
        [DebuggerStepThrough]
        public void AllocateRegion(BlockRegion region)
        {
            try
            {
                FDataMap.AllocateRegion(region);
            }
            catch (InvalidAllocationException)
            {
                throw new DataAreaFullException();
            }
        }

        /// <summary>
        /// Attempts to allocate the <paramref name="region"/> specified.
        /// </summary>
        /// <param name="region">The region to be allocated.</param>
        /// <exception cref="InvalidAllocationException">The requested region cannot be allocated.</exception>
        /// <remarks>Used by index area validation code. The exception is not remapped to <see cref="DataAreaFullException"/>.</remarks>
        [DebuggerStepThrough]
        public void AllocateRegionForValidation(BlockRegion region)
        {
            FDataMap.AllocateRegion(region);
        }

        /// <summary>
        /// Deallocates a region of blocks in the data area.
        /// </summary>
        /// <param name="region">The region to deallocate.</param>
        /// <exception cref="ArgumentException">The argument <paramref name="region"/> specifies an invalid, overlapping or not allocated region.</exception>
        [DebuggerStepThrough]
        public void DeleteFile(BlockRegion region)
        {
            FDataMap.DeallocateRegion(region);
        }

        /// <summary>
        /// Registers a region of index area entries for recycling.
        /// </summary>
        /// <param name="region">The region.</param>
        /// <param name="callback">The callback.</param>
        [DebuggerStepThrough]
        public void Recycle(BlockRegion region, IEntryRecycleCallback callback)
        {
            FDataMap.DeallocateRegion(region);
        }

        /// <summary>
        /// Reserves the specified region, taking preexisting allocations into account.
        /// </summary>
        /// <param name="region">The region to reserve.</param>
        [DebuggerStepThrough]
        public void ReserveRegion(BlockRegion region)
        {
            FDataMap.ReserveRegion(region);
        }

        #region Read blocks

        /// <summary>
        /// Reads blocks from the data area.
        /// </summary>
        /// <param name="index">The zero-based index of the block to read.</param>
        /// <param name="count">The count of blocks to read.</param>
        /// <returns>The contents of the data area block specified by <paramref name="index"/>.</returns>
        [DebuggerStepThrough]
        public byte[] Read(long index, int count)
        {
            // The specification is not exactly clear whether the data block with index zero
            // is the first block of the data area, or the first block of the volume.
            // Lets assume its the first block of the volume.
            return FVolume.Read(index * FBlockSize, count * FBlockSize);
        }

        #endregion

        #region Write blocks

        /// <summary>
        /// Writes the blocks to the data area.
        /// </summary>
        /// <param name="block">The zero-based index of the block to write.</param>
        /// <param name="buffer">The buffer to copy the data from, which must be a multiple of the block size.</param>
        [DebuggerStepThrough]
        public void Write(long block, byte[] buffer)
        {
            Debug.Assert((buffer.Length % FBlockSize) == 0);
            // The specification is not exactly clear whether the data block with index zero
            // is the first block of the data area, or the first block of the volume.
            // Lets assume its the first block of the volume.
            FVolume.Write(block * FBlockSize, buffer);
        }

        /// <summary>
        /// Writes blocks to the data area.
        /// </summary>
        /// <param name="block">The zero-based index of the block to write.</param>
        /// <param name="buffer">The buffer to copy the data from. May be not a multiple of block size.</param>
        /// <param name="index">The zero-based index into <paramref name="buffer"/>.</param>
        /// <param name="count">The count of blocks to write.</param>
        [DebuggerStepThrough]
        public void Write(long block, byte[] buffer, int index, int count)
        {
            Debug.Assert(index + count * FBlockSize <= buffer.Length);
            // See above.
            FVolume.Write(block * FBlockSize, buffer, index, count * FBlockSize);
        }

        #endregion

        #endregion

        #region Constructors
        /// <overloads>Initializes a new instance of <see cref="DataArea"/>.</overloads>
        /// <summary>
        /// Initializes a new instance of <see cref="DataArea"/>.
        /// </summary>
        /// <param name="volume">The volume.</param>
        /// <param name="length">The count of blocks in the data area.</param>
        /// <param name="count">The count of blocks in the volume.</param>
        [DebuggerStepThrough]
        public DataArea(IVolume volume, long length, long count)
            : base()
        {
            FVolume = volume ?? new NullVolume();
            //FPosition = position;
            FLength = length;
            FVolumeLength = count;
            FDataMap = new DataAreaBlockRegionMap(count);
        }
        #endregion

        #region Helper classes

        /// <summary>
        /// Implements a dummy volume.
        /// </summary>
        private class NullVolume : Object, IVolume
        {
            #region Constructors
            /// <overloads>Initializes a new instance of <see cref="NullVolume"/>.</overloads>
            /// <summary>
            /// Initializes a new instance of <see cref="NullVolume"/>.
            /// </summary>
            [DebuggerStepThrough]
            public NullVolume()
                : base()
            {
            }
            #endregion

            #region IVolume Members

            /// <summary>
            /// Gets the length of the volume, in bytes.
            /// </summary>
            /// <exception cref="NotSupportedException">An attempt was made to query the size of the volume on a volume which does not support this operation.</exception>
            Int64 IVolume.Length
            {
                [DebuggerStepThrough]
                get
                {
                    throw new NotSupportedException();
                }
            }

            /// <summary>
            /// Reads <paramref name="count"/> bytes from the volume.
            /// </summary>
            /// <param name="offset">The byte offset to start reading from.</param>
            /// <param name="count">The count of bytes read.</param>
            /// <returns>An array contaning the bytes read.</returns>
            /// <exception cref="EndOfStreamException">There is not enough space to read all the bytes (more bytes requested than the volume contains).</exception>
            /// <exception cref="ArgumentOutOfRangeException">The argument <paramref name="count"/> is out of valid range.</exception>
            [DebuggerStepThrough]
            byte[] IVolume.Read(long offset, int count)
            {
                return new byte[count];
            }

            /// <summary>
            /// Writes the contents of <paramref name="buffer"/> to the volume.
            /// </summary>
            /// <param name="offset">The byte offset to start writing at.</param>
            /// <param name="buffer">The array containing the bytes to write.</param>
            /// <exception cref="EndOfStreamException">There is not enough space to write all the bytes.</exception>
            /// <exception cref="ArgumentNullException">The argument <paramref name="buffer"/> is <see langword="null"/>.</exception>
            /// <exception cref="IOException">An I/O error occurred.</exception>
            [DebuggerStepThrough]
            void IVolume.Write(long offset, byte[] buffer)
            {
                //
            }

            /// <summary>
            /// Writes the contents of <paramref name="buffer"/> to the volume.
            /// </summary>
            /// <param name="offset">The byte offset to start writing at.</param>
            /// <param name="buffer">The array containing the bytes to write.</param>
            /// <param name="index">The index into <paramref name="buffer"/> to copy the bytes from.</param>
            /// <param name="count">The count of bytes to copy.</param>
            /// <exception cref="EndOfStreamException">There is not enough space to write all the bytes.</exception>
            /// <exception cref="ArgumentNullException">The argument <paramref name="buffer"/> is <see langword="null"/>.</exception>
            /// <exception cref="IOException">An I/O error occurred.</exception>
            [DebuggerStepThrough]
            void IVolume.Write(long offset, byte[] buffer, int index, int count)
            {
                //
            }

            #endregion

            #region IDisposable Members

            /// <summary>
            /// Releases all resources used by this <see cref="NullVolume"/> instance.
            /// </summary>
            [DebuggerStepThrough]
            void IDisposable.Dispose()
            {
                //
            }

            #endregion
        }

        #endregion
    }
}
